package zpengblog.taskdemo.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 实现分布式redis锁.
 *
 * @author linzp
 * @version 1.0.0
 * CreateDate 2021/1/14 13:53
 */
@Component
public class RedisLockUtils {

    /**
     * 锁名称前缀.
     */
    public static final String TASK_LOCK_PREFIX = "TASK_LOCK_";

    /**
     * redis操作.
     */
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 获取分布式redis锁.
     * 逻辑：
     * 1、使用setNX原子操作设置锁（返回 true-代表加锁成功，false-代表加锁失败）
     * 2、加锁成功直接返回
     * 3、加锁失败，进行监测，是否存在死锁的情况，检查上一个锁是否已经过期
     * 4、如果过期，重新让当前线程获取新的锁。
     * 5、这里可能会出现多个获取锁失败的线程执行到这一步，所以判断是否是加锁成功，如果没有，则返回失败
     *
     * @param taskName       任务名称
     * @param lockExpireTime 锁的过期时间
     * @return true-获取锁成功 false-获取锁失败
     */
    public Boolean getLock(String taskName, long lockExpireTime) {
        //锁的名称：前缀 + 任务名称
        String lockName = TASK_LOCK_PREFIX + taskName;

        return (Boolean) redisTemplate.execute((RedisCallback<?>) connection -> {
            // 计算此次过期时间：当前时间往后延申一个expireTIme
            long expireAt = System.currentTimeMillis() + lockExpireTime + 1;
            // 获取锁（setNX 原子操作）
            Boolean acquire = connection.setNX(lockName.getBytes(), String.valueOf(expireAt).getBytes());
            // 如果设置成功
            if (Objects.nonNull(acquire) && acquire) {
                return true;
            } else {
                //防止死锁，获取旧的过期时间，与当前系统时间比是否过期，如果过期则允许其他的线程再次获取。
                byte[] bytes = connection.get(lockName.getBytes());
                if (Objects.nonNull(bytes) && bytes.length > 0) {
                    long expireTime = Long.parseLong(new String(bytes));
                    // 如果旧的锁已经过期
                    if (expireTime < System.currentTimeMillis()) {
                        // 重新让当前线程加锁
                        byte[] oldLockExpireTime = connection.getSet(lockName.getBytes(),
                                String.valueOf(System.currentTimeMillis() + lockExpireTime + 1).getBytes());
                        //这里为null证明这个新锁加锁成功，上一个旧锁已被释放
                        if (Objects.isNull(oldLockExpireTime)) {
                            return true;
                        }
                        // 防止在并发场景下，被其他线程抢先加锁成功的问题
                        return Long.parseLong(new String(oldLockExpireTime)) < System.currentTimeMillis();
                    }
                }
            }
            return false;
        });
    }

    /**
     * 删除锁.
     * 这里可能会存在一种异常情况，即如果线程A加锁成功
     * 但是由于io或GC等原因在有效期内没有执行完逻辑，这时线程B也可拿到锁去执行。
     * （方案，可以加锁的时候新增当前线程的id作为标识，释放锁时，判断一下，只能释放自己加的锁）
     *
     * @param lockName 锁名称
     */
    public void delLock(String lockName) {
        // 直接删除key释放redis锁
        redisTemplate.delete(lockName);
    }
}
